<?php
/*******************************************************************************
* Utility to generate font definition files                                    *
*                                                                              *
* Version: 1.3                                                                 *
* Date:    2015-11-29                                                          *
* Author:  Olivier PLATHEY                                                     *
*******************************************************************************/

require 'ttfparser.php';

function Message($txt, $severity = '')
{
    if (PHP_SAPI == 'cli') {
        if ($severity) {
            echo "$severity: ";
        }
        echo "$txt\n";
    } else {
        if ($severity) {
            echo "<b>$severity</b>: ";
        }
        echo "$txt<br>";
    }
}

function Notice($txt)
{
    Message($txt, 'Notice');
}

function Warning($txt)
{
    Message($txt, 'Warning');
}

function Error($txt)
{
    Message($txt, 'Error');
    exit;
}

function LoadMap($enc)
{
    $file = dirname(__FILE__) . '/' . strtolower($enc) . '.map';
    $a = file($file);
    if (empty($a)) {
        Error('Encoding not found: ' . $enc);
    }
    $map = array_fill(0, 256, ['uv' => -1, 'name' => '.notdef']);
    foreach ($a as $line) {
        $e = explode(' ', rtrim($line));
        $c = hexdec(substr($e[0], 1));
        $uv = hexdec(substr($e[1], 2));
        $name = $e[2];
        $map[$c] = ['uv' => $uv, 'name' => $name];
    }
    return $map;
}

function GetInfoFromTrueType($file, $embed, $subset, $map)
{
    // Return information from a TrueType font
    try {
        $ttf = new TTFParser($file);
        $ttf->Parse();
    } catch (Exception $e) {
        Error($e->getMessage());
    }
    if ($embed) {
        if (!$ttf->embeddable) {
            Error('Font license does not allow embedding');
        }
        if ($subset) {
            $chars = [];
            foreach ($map as $v) {
                if ($v['name'] != '.notdef') {
                    $chars[] = $v['uv'];
                }
            }
            $ttf->Subset($chars);
            $info['Data'] = $ttf->Build();
        } else {
            $info['Data'] = file_get_contents($file);
        }
        $info['OriginalSize'] = strlen($info['Data']);
    }
    $k = 1000 / $ttf->unitsPerEm;
    $info['FontName'] = $ttf->postScriptName;
    $info['Bold'] = $ttf->bold;
    $info['ItalicAngle'] = $ttf->italicAngle;
    $info['IsFixedPitch'] = $ttf->isFixedPitch;
    $info['Ascender'] = round($k * $ttf->typoAscender);
    $info['Descender'] = round($k * $ttf->typoDescender);
    $info['UnderlineThickness'] = round($k * $ttf->underlineThickness);
    $info['UnderlinePosition'] = round($k * $ttf->underlinePosition);
    $info['FontBBox'] = [round($k * $ttf->xMin), round($k * $ttf->yMin), round($k * $ttf->xMax), round($k * $ttf->yMax)];
    $info['CapHeight'] = round($k * $ttf->capHeight);
    $info['MissingWidth'] = round($k * $ttf->glyphs[0]['w']);
    $widths = array_fill(0, 256, $info['MissingWidth']);
    foreach ($map as $c => $v) {
        if ($v['name'] != '.notdef') {
            if (isset($ttf->chars[$v['uv']])) {
                $id = $ttf->chars[$v['uv']];
                $w = $ttf->glyphs[$id]['w'];
                $widths[$c] = round($k * $w);
            } else {
                Warning('Character ' . $v['name'] . ' is missing');
            }
        }
    }
    $info['Widths'] = $widths;
    return $info;
}

function GetInfoFromType1($file, $embed, $map)
{
    // Return information from a Type1 font
    if ($embed) {
        $f = fopen($file, 'rb');
        if (!$f) {
            Error('Can\'t open font file');
        }
        // Read first segment
        $a = unpack('Cmarker/Ctype/Vsize', fread($f, 6));
        if ($a['marker'] != 128) {
            Error('Font file is not a valid binary Type1');
        }
        $size1 = $a['size'];
        $data = fread($f, $size1);
        // Read second segment
        $a = unpack('Cmarker/Ctype/Vsize', fread($f, 6));
        if ($a['marker'] != 128) {
            Error('Font file is not a valid binary Type1');
        }
        $size2 = $a['size'];
        $data .= fread($f, $size2);
        fclose($f);
        $info['Data'] = $data;
        $info['Size1'] = $size1;
        $info['Size2'] = $size2;
    }

    $afm = substr($file, 0, -3) . 'afm';
    if (!file_exists($afm)) {
        Error('AFM font file not found: ' . $afm);
    }
    $a = file($afm);
    if (empty($a)) {
        Error('AFM file empty or not readable');
    }
    foreach ($a as $line) {
        $e = explode(' ', rtrim($line));
        if (count($e) < 2) {
            continue;
        }
        $entry = $e[0];
        if ($entry == 'C') {
            $w = $e[4];
            $name = $e[7];
            $cw[$name] = $w;
        } elseif ($entry == 'FontName') {
            $info['FontName'] = $e[1];
        } elseif ($entry == 'Weight') {
            $info['Weight'] = $e[1];
        } elseif ($entry == 'ItalicAngle') {
            $info['ItalicAngle'] = (int)$e[1];
        } elseif ($entry == 'Ascender') {
            $info['Ascender'] = (int)$e[1];
        } elseif ($entry == 'Descender') {
            $info['Descender'] = (int)$e[1];
        } elseif ($entry == 'UnderlineThickness') {
            $info['UnderlineThickness'] = (int)$e[1];
        } elseif ($entry == 'UnderlinePosition') {
            $info['UnderlinePosition'] = (int)$e[1];
        } elseif ($entry == 'IsFixedPitch') {
            $info['IsFixedPitch'] = ($e[1] == 'true');
        } elseif ($entry == 'FontBBox') {
            $info['FontBBox'] = [(int)$e[1], (int)$e[2], (int)$e[3], (int)$e[4]];
        } elseif ($entry == 'CapHeight') {
            $info['CapHeight'] = (int)$e[1];
        } elseif ($entry == 'StdVW') {
            $info['StdVW'] = (int)$e[1];
        }
    }

    if (!isset($info['FontName'])) {
        Error('FontName missing in AFM file');
    }
    if (!isset($info['Ascender'])) {
        $info['Ascender'] = $info['FontBBox'][3];
    }
    if (!isset($info['Descender'])) {
        $info['Descender'] = $info['FontBBox'][1];
    }
    $info['Bold'] = isset($info['Weight']) && preg_match('/bold|black/i', $info['Weight']);
    if (isset($cw['.notdef'])) {
        $info['MissingWidth'] = $cw['.notdef'];
    } else {
        $info['MissingWidth'] = 0;
    }
    $widths = array_fill(0, 256, $info['MissingWidth']);
    foreach ($map as $c => $v) {
        if ($v['name'] != '.notdef') {
            if (isset($cw[$v['name']])) {
                $widths[$c] = $cw[$v['name']];
            } else {
                Warning('Character ' . $v['name'] . ' is missing');
            }
        }
    }
    $info['Widths'] = $widths;
    return $info;
}

function MakeFontDescriptor($info)
{
    // Ascent
    $fd = "array('Ascent'=>" . $info['Ascender'];
    // Descent
    $fd .= ",'Descent'=>" . $info['Descender'];
    // CapHeight
    if (!empty($info['CapHeight'])) {
        $fd .= ",'CapHeight'=>" . $info['CapHeight'];
    } else {
        $fd .= ",'CapHeight'=>" . $info['Ascender'];
    }
    // Flags
    $flags = 0;
    if ($info['IsFixedPitch']) {
        $flags += 1 << 0;
    }
    $flags += 1 << 5;
    if ($info['ItalicAngle'] != 0) {
        $flags += 1 << 6;
    }
    $fd .= ",'Flags'=>" . $flags;
    // FontBBox
    $fbb = $info['FontBBox'];
    $fd .= ",'FontBBox'=>'[" . $fbb[0] . ' ' . $fbb[1] . ' ' . $fbb[2] . ' ' . $fbb[3] . "]'";
    // ItalicAngle
    $fd .= ",'ItalicAngle'=>" . $info['ItalicAngle'];
    // StemV
    if (isset($info['StdVW'])) {
        $stemv = $info['StdVW'];
    } elseif ($info['Bold']) {
        $stemv = 120;
    } else {
        $stemv = 70;
    }
    $fd .= ",'StemV'=>" . $stemv;
    // MissingWidth
    $fd .= ",'MissingWidth'=>" . $info['MissingWidth'] . ')';
    return $fd;
}

function MakeWidthArray($widths)
{
    $s = "array(\n\t";
    for ($c = 0;$c <= 255;$c++) {
        if (chr($c) == "'") {
            $s .= "'\\''";
        } elseif (chr($c) == '\\') {
            $s .= "'\\\\'";
        } elseif ($c >= 32 && $c <= 126) {
            $s .= "'" . chr($c) . "'";
        } else {
            $s .= "chr($c)";
        }
        $s .= '=>' . $widths[$c];
        if ($c < 255) {
            $s .= ',';
        }
        if (($c + 1) % 22 == 0) {
            $s .= "\n\t";
        }
    }
    $s .= ')';
    return $s;
}

function MakeFontEncoding($map)
{
    // Build differences from reference encoding
    $ref = LoadMap('cp1252');
    $s = '';
    $last = 0;
    for ($c = 32;$c <= 255;$c++) {
        if ($map[$c]['name'] != $ref[$c]['name']) {
            if ($c != $last + 1) {
                $s .= $c . ' ';
            }
            $last = $c;
            $s .= '/' . $map[$c]['name'] . ' ';
        }
    }
    return rtrim($s);
}

function MakeUnicodeArray($map)
{
    // Build mapping to Unicode values
    $ranges = [];
    foreach ($map as $c => $v) {
        $uv = $v['uv'];
        if ($uv != -1) {
            if (isset($range)) {
                if ($c == $range[1] + 1 && $uv == $range[3] + 1) {
                    $range[1]++;
                    $range[3]++;
                } else {
                    $ranges[] = $range;
                    $range = [$c, $c, $uv, $uv];
                }
            } else {
                $range = [$c, $c, $uv, $uv];
            }
        }
    }
    $ranges[] = $range;

    foreach ($ranges as $range) {
        if (isset($s)) {
            $s .= ',';
        } else {
            $s = 'array(';
        }
        $s .= $range[0] . '=>';
        $nb = $range[1] - $range[0] + 1;
        if ($nb > 1) {
            $s .= 'array(' . $range[2] . ',' . $nb . ')';
        } else {
            $s .= $range[2];
        }
    }
    $s .= ')';
    return $s;
}

function SaveToFile($file, $s, $mode)
{
    $f = fopen($file, 'w' . $mode);
    if (!$f) {
        Error('Can\'t write to file ' . $file);
    }
    fwrite($f, $s);
    fclose($f);
}

function MakeDefinitionFile($file, $type, $enc, $embed, $subset, $map, $info)
{
    $s = "<?php\n";
    $s .= '$type = \'' . $type . "';\n";
    $s .= '$name = \'' . $info['FontName'] . "';\n";
    $s .= '$desc = ' . MakeFontDescriptor($info) . ";\n";
    $s .= '$up = ' . $info['UnderlinePosition'] . ";\n";
    $s .= '$ut = ' . $info['UnderlineThickness'] . ";\n";
    $s .= '$cw = ' . MakeWidthArray($info['Widths']) . ";\n";
    $s .= '$enc = \'' . $enc . "';\n";
    $diff = MakeFontEncoding($map);
    if ($diff) {
        $s .= '$diff = \'' . $diff . "';\n";
    }
    $s .= '$uv = ' . MakeUnicodeArray($map) . ";\n";
    if ($embed) {
        $s .= '$file = \'' . $info['File'] . "';\n";
        if ($type == 'Type1') {
            $s .= '$size1 = ' . $info['Size1'] . ";\n";
            $s .= '$size2 = ' . $info['Size2'] . ";\n";
        } else {
            $s .= '$originalsize = ' . $info['OriginalSize'] . ";\n";
            if ($subset) {
                $s .= "\$subsetted = true;\n";
            }
        }
    }
    $s .= "?>\n";
    SaveToFile($file, $s, 't');
}

function MakeFont($fontfile, $enc = 'cp1252', $embed = true, $subset = true)
{
    // Generate a font definition file
    if (get_magic_quotes_runtime()) {
        @set_magic_quotes_runtime(false);
    }
    ini_set('auto_detect_line_endings', '1');

    if (!file_exists($fontfile)) {
        Error('Font file not found: ' . $fontfile);
    }
    $ext = strtolower(substr($fontfile, -3));
    if ($ext == 'ttf' || $ext == 'otf') {
        $type = 'TrueType';
    } elseif ($ext == 'pfb') {
        $type = 'Type1';
    } else {
        Error('Unrecognized font file extension: ' . $ext);
    }

    $map = LoadMap($enc);

    if ($type == 'TrueType') {
        $info = GetInfoFromTrueType($fontfile, $embed, $subset, $map);
    } else {
        $info = GetInfoFromType1($fontfile, $embed, $map);
    }

    $basename = substr(basename($fontfile), 0, -4);
    if ($embed) {
        if (function_exists('gzcompress')) {
            $file = $basename . '.z';
            SaveToFile($file, gzcompress($info['Data']), 'b');
            $info['File'] = $file;
            Message('Font file compressed: ' . $file);
        } else {
            $info['File'] = basename($fontfile);
            $subset = false;
            Notice('Font file could not be compressed (zlib extension not available)');
        }
    }

    MakeDefinitionFile($basename . '.php', $type, $enc, $embed, $subset, $map, $info);
    Message('Font definition file generated: ' . $basename . '.php');
}

if (PHP_SAPI == 'cli') {
    // Command-line interface
    ini_set('log_errors', '0');
    if ($argc == 1) {
        die("Usage: php makefont.php fontfile [encoding] [embed] [subset]\n");
    }
    $fontfile = $argv[1];
    if ($argc >= 3) {
        $enc = $argv[2];
    } else {
        $enc = 'cp1252';
    }
    if ($argc >= 4) {
        $embed = ($argv[3] == 'true' || $argv[3] == '1');
    } else {
        $embed = true;
    }
    if ($argc >= 5) {
        $subset = ($argv[4] == 'true' || $argv[4] == '1');
    } else {
        $subset = true;
    }
    MakeFont($fontfile, $enc, $embed, $subset);
}
